/*jslint maxerr: 50, indent: 4, browser: true */

/* isEqual v1.0
 *
 * Add functionality that compares two objects to see if they are
 * comparitivley equal, additional option allows for the objects to be
 * sorted and then compared. This is quite an intensive process and can
 * be relatively slow for large objects containing many properties.
 * The process becomes even slower if the sort option is enabled.
 * Written with cross-browser compatibility in mind.
 *
 * Copyright (C) 2013  Graham Fairweather (a.k.a: Xotic750)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

var isEqual = (function (mode, undef) {
    "use strict";

    var baseArray = [], // our base array reference
        baseObject = {}, // our base object reference
        toStringFn = baseObject.toString, // for obtaining object class strings
        strArrayClass = toStringFn.call(baseArray), // capture the array class string
        strFunctionClass = toStringFn.call(toStringFn), // capture the function class string
        CtrObject = baseObject.constructor,
        proto = "__proto__", // work around for jslint complaint
        supportsProto = (CtrObject.prototype[proto] === null), // is "__proto__" supported
        hasOwnPropertyFn = baseObject.hasOwnProperty, // to combat old IE8- issues, min support IE6
        isPrototypeOfFn = baseObject.isPrototypeOf,
        keysFn = CtrObject.keys,
        getPrototypeOf = CtrObject.getPrototypeOf,
        isArray = baseArray.constructor.isArray,
        is = CtrObject.is,
        hasOwnProperty,
        instanceOf,
        someOwnProperties,
        deepSort;

    // http://ecma-international.org/ecma-262/5.1/#sec-15.2.4.5
    // Create our own local "hasOwnProperty" function: native -> fallback
    if (!mode && typeof hasOwnPropertyFn === "function") {
        hasOwnProperty = function (object, property) {
            return hasOwnPropertyFn.call(object, property);
        };
    } else {
        hasOwnProperty = function (object, property) {
            return (property in object) && (object.constructor.prototype[property] === undef);
        };
    }

    // http://ecma-international.org/ecma-262/5.1/#sec-15.2.3.2
    // Create our own local "getPrototypeOf" function: native -> "__proto__" polyfill -> fallback
    if ((!mode && typeof getPrototypeOf !== "function") || (mode === 1 && supportsProto)) {
        getPrototypeOf = function (object) {
            return object[proto];
        };
    } else {
        getPrototypeOf = function (object) {
            return object.constructor;
        };
    }

    // http://ecma-international.org/ecma-262/5.1/#sec-11.8.6
    // Create our own local "instanceOf" using "isPrototypeOf" function: native -> "__proto__" polyfill -> fallback
    if (!mode && typeof isPrototypeOfFn === "function") {
        instanceOf = function (object, func) {
            return isPrototypeOfFn.call(func.prototype, object);
        };
    } else if (mode === 1 && supportsProto) {
        instanceOf = function (object, func) {
            if (typeof func !== "function" && func !== null && typeof object === "object" && toStringFn.call(object) !== strFunctionClass) {
                throw new TypeError("Expecting a function in instanceOf check.");
            }

            if (object === null || typeof object !== "object") {
                return false;
            }

            var prototype = func.prototype;

            while (object) {
                if (object === prototype) {
                    return true;
                }

                object = object[proto];
            }

            return false;
        };
    } else {
        instanceOf = function (object, func) {
            return object instanceof func;
        };
    }

    // ES5 polyfill
    // http://ecma-international.org/ecma-262/5.1/#sec-15.4.3.2
    if (mode || typeof isArray !== "function") {
        isArray = function (object) {
            return object !== null && typeof object === "object" && toStringFn.call(object) === strArrayClass;
        };
    }

    // ES6 polyfill
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
    if (!mode || typeof is !== "function") {
        is = function (x, y) {
            if (x === y) {
                if (x === 0) {
                    return 1 / x === 1 / y;
                }

                return true;
            }

            return x !== x && y !== y;
        };
    }

    // http://ecma-international.org/ecma-262/5.1/#sec-15.2.3.14
    // Create our own local "someOwnProperties" using "Object.keys" function: native -> for..in with hasOwnProperty
    if (!mode && typeof keysFn === "function") {
        someOwnProperties = function (object, func) {
            var keys = keysFn(object),
                length,
                index;

            for (index = 0, length = keys.length; index < length; index += 2) {
                if (func(keys[index], object[keys[index]]) === false) {
                    return false;
                }
            }

            return true;
        };
    } else {
        someOwnProperties = function (object, func) {
            var property;

            for (property in object) {
                if (hasOwnProperty(object, property) && func(property, object[property]) === false) {
                    return false;
                }
            }

            return true;
        };
    }

    // Get a string representation of the object for use with the sort comparison
    function getString(object) {
        if (object === undef) {
            return "[object Undefined]undefined";
        }

        if (object === null) {
            return "[object Null]null";
        }

        var string = object.toString();

        if (/^\[object [\s\S]+\]$/.test(string)) {
            someOwnProperties(object, function (property, value) {
                string += (property + value);
            });

            return string;
        }

        return toStringFn.call(object) + string;
    }

    // Custom sort comparitor to give a repeatable sort order
    function customComparison(left, right) {
        if (isArray(left)) {
            left = strArrayClass + deepSort(left).toString();
        } else {
            left = getString(left);
        }

        if (isArray(right)) {
            right = strArrayClass + deepSort(right).toString();
        } else {
            right = getString(right);
        }

        if (left === right) {
            return 0;
        }

        if (left < right) {
            return -1;
        }

        return 1;
    }

    // Recursive sort function to give a repeatable sort order
    deepSort = function (array) {
        array = array.slice().sort(customComparison);

        var length,
            element,
            i;

        for (i = 0, length = array.length; i < length; i += 1) {
            element = array[i];
            if (isArray(element)) {
                array[i] = deepSort(element);
            }
        }

        return array;
    };

    // Set "performSort" to "true" if you want to compare arrays of any order, much slower. Default: false
    return function isEqual(object1, object2, performSort) {
        if (is(object1, object2)) {
            return true;
        }

        if (toStringFn.call(object1) !== toStringFn.call(object2)) {
            return false;
        }

        if (object1 !== undef && object1 !== null) {
            object1 = object1.valueOf();
            object2 = object2.valueOf();
        }

        if (is(object1, object2)) {
            return true;
        }

        if (!instanceOf(object1, Object) || !instanceOf(object2, Object) || getPrototypeOf(object1) !== getPrototypeOf(object2)) {
            return false;
        }

        if (isArray(object1) && isArray(object2)) {
            if (object1.length !== object2.length) {
                return false;
            }

            if (performSort) {
                object1 = deepSort(object1);
                object2 = deepSort(object2);
            }
        }

        var equal = someOwnProperties(object1, function (property, element1, element2, undef) {
            if (!hasOwnProperty(object2, property)) {
                return false;
            }

            element2 = object2[property];
            if (!is(element1, element2)) {
                if (toStringFn.call(element1) !== toStringFn.call(element2)) {
                    return false;
                }

                if (element1 !== undef && element1 !== null) {
                    element1 = element1.valueOf();
                    element2 = element2.valueOf();
                }

                if (!is(element1, element2) && !isEqual(element1, element2, performSort)) {
                    return false;
                }
            }

            return true;
        });

        if (!equal) {
            return false;
        }

        return someOwnProperties(object2, function (property) {
            if (!hasOwnProperty(object1, property)) {
                return false;
            }

            return true;
        });
    };
}());

/*jslint devel: true, white: true, regexp: true */
/*global $ */

var test1 = [1, "a", true, NaN, 2, , , undefined, 3, false, null, new Date(2012, 7, 12), "1,2,test", /./g],
    test2 = [1, "a", new Boolean(true), NaN, new Number(2), , , undefined, 3, false, null, new Date(2012, 7, 12), new String("1,2,test"), new RegExp(".", "g")],
    test3 = ["a", [],
        ["a", "b"], 1, 2, [
            [1, ["a", "b"], 3],
            [3, 2, 1, 9, 8, ["a", "b"]],
            [4, [
                [1, 2, 3],
                [test2, 2, 1, ["x", "y", "z"], test1, [3, 2, 1]],
                [4, 5]
            ]]
        ], 3, [3, NaN, 2, null, 1, undefined]
    ],
    test4 = [
        [3, null, 2, undefined, 1, NaN], 1, [
            [1, 2, 3, ["b", "a"], 8, 9],
            [
                [
                    [1, 2, test1, [1, 2, 3], test2, ["z", "y", "x"]],
                    [5, 4],
                    [3, 2, 1]
                ], 4],
            [3, ["b", "a"], 1]
        ],
        ["b", "a"], 2, 3, "a", []
    ],
    test5 = {
        a: test1,
        b: test2,
        c: test3,
        d: test4
    },
    test6 = {
        a: test1.slice(),
        b: test2.slice(),
        c: test3.slice(),
        d: test4.slice()
    },
    test7 = {
        a: [1, "a", true, NaN, 2, , , undefined, 3, false, null, new Date(2012, 7, 12), "1,2,test", /./g],
        b: [1, "a", new Boolean(true), NaN, new Number(2), , , undefined, 3, false, null, new Date(2012, 7, 12), new String("1,2,test"), new RegExp(".", "g")],
        c: ["a", [],
            ["a", "b"], 1, 2, [
                [1, ["a", "b"], 3],
                [3, 2, 1, 9, 8, ["a", "b"]],
                [4, [
                    [1, 2, 3],
                    [test2, 2, 1, ["x", "y", "z"], test1, [3, 2, 1]],
                    [4, 5]
                ]]
            ], 3, [3, NaN, 2, null, 1, undefined]
        ],
        d: [
            [3, null, 2, undefined, 1, NaN], 1, [
                [1, 2, 3, ["b", "a"], 8, 9],
                [
                    [
                        [1, 2, test1, [1, 2, 3], test2, ["z", "y", "x"]],
                        [5, 4],
                        [3, 2, 1]
                    ], 4],
                [3, ["b", "a"], 1]
            ],
            ["b", "a"], 2, 3, "a", []
        ]
    },
    test8 = {
        a: [1, "a", true, NaN, 2, , , undefined, 3, false, null, new Date(2012, 7, 12), "1,2,test", /./g],
        b: [1, "a", new Boolean(true), NaN, new Number(2), , , undefined, 3, false, null, new Date(2012, 7, 12), new String("1,2,test"), new RegExp(".", "g")],
        c: test3,
        d: test4
    },
    test9 = {
        a: [1, "a", true, NaN, 2, , , undefined, 3, false, null, new Date(2012, 7, 12), "1,2,test", /./g],
        b: [1, "a", new Boolean(true), NaN, new Number(2), , , undefined, 3, false, null, new Date(2012, 7, 12), new String("1,2,test"), new RegExp(".", "g")],
        c: test4,
        d: test3
    };

console.log(true, isEqual(test1, test2));
console.log(true, isEqual(test1, test2));
console.log(false, isEqual(test3, test4));
console.log(true, isEqual(test5, test6));
console.log(true, isEqual(test5, test7));
console.log(true, isEqual(test6, test7));
console.log(true, isEqual(test7, test8));
console.log(false, isEqual(test7, test9));
console.log(true, isEqual(test7, test9, true));
console.log(false, isEqual(test8, test9));
console.log(true, isEqual(test8, test9, true));
console.log(true, isEqual(document.getElementById("test1"), document.getElementById("test1")));
console.log(false, isEqual(document.getElementById("test1"), document.getElementById("test2")));
console.log(true, isEqual(document.getElementsByTagName("*"), document.getElementsByTagName("*")));
console.log(true, isEqual(window, window.window));
console.log(true, isEqual(document.body, document.getElementsByTagName("body")[0]));
console.log(true, isEqual($("#test1"), $("#test1")));
console.log(false, isEqual($("#test1"), $("#test2")));
console.log(true, isEqual($("div"), $("div")));
console.log(true, isEqual(document.body, $("body").get(0)));
console.log(false, isEqual(window, window.frames[0]));
console.log(false, isEqual(window, test9));
console.log(false, isEqual([window, 1, [2, 1], document, {}, 2], [document, [1, 2], 2, {},
1, window]));
console.log(true, isEqual([window, 1, {}, [2, 1], document, {},
2], [document, [1, 2], 2, {},
1, {},
window], true));

var parent = document.body || document.documentElement,
    javascript = "javascript";

function getReferenceObject(name) {
    var iframe = document.createElement('iframe'),
        reference;

    iframe.style.display = "none";
    iframe.src = javascript + ":";
    parent.appendChild(iframe);
    reference = iframe.contentWindow[name];
    parent.removeChild(iframe);
    iframe = null;
    return reference;
}

console.log(false, isEqual(Object, getReferenceObject("Object")));
console.log(true, isEqual(1, new Number(1)));
console.log(true, isEqual("a", new String("a")));
console.log(true, isEqual(true, new Boolean(true)));
